------------------ EXPANDING NES ROMS ------------------ An interesting question brought up is how do I go about expanding NES ROMs? *******FINDING THE ORIGINAL SIZE AND MAPPER************* Well, what I find the safest way is to figure out what the PRG and CHR sizes of the NES ROM are. Some emulators may be able to give us this information: - With Nesten, just use Nesten's custom ROM loading menu, and not the standard Windows File Open thingey - With FCEUltra, check the Help->Messages. It will tell you how many PRG and CHR banks there are, and the sizes of each. So, you multiply bank quantity x bank size. So like PRG: 16x 8 KB would mean 128KB. NOTE: For expansion purposes, games with both PRG and CHR should be seen as two ROMs that just happen to be joined together in one computer file. When I say ROM, I will be referring only to the one of those two ROMs that is being hacked. "ROM file" will be used to refer to the computer file with the conjoined ROMs. That is important, because from what I've seen, ROM expansion only works at power-of-2 sizes, meaning you'd need to double the PRG ROM size (or CHR if you're trying to add more graphics to a game that uses CHR). *******CAN THE GAME BE EXPANDED?********************** But, you will first want to find out what mapper the game you want to hack uses. Then you will want to go to a place like nesdev.parodius.com and find a document on the mapper. Then determine the maximum size your game's mapper can support. Is the PRG/CHR (whichever you're hacking) already at maximum? If not, good you can expand it. If you can double the ROM's size to stay within the maximum, then good. I haven't had luck doing any non-size-doubling hacks. *********EXPANDING THE ROM PHYSICALLY***************** Now, to double a ROM size. Use a hex editor that supports copy and paste functions. For Windows, I would suggest "A.X.E." as it's a freeware editor that can serve our purpose. Do a Google search for "A.X.E. Hex editor" and you should find it. I'm hoping you can figure out how to setup the editor to display hex offsets correctly. Now, we will want to open the original ROM file, and copy the iNES header (first 0x10 bytes), and paste them into a new file. (Since you are overwriting the 0x00 that is already there, you must highlight the 0x00, then click Paste. Now, if we are duplicating the PRG ROM, we will want to figure out the hexadecimal size of the PRG. Windows calculator will help with that. For example, 128KB = 131072 bytes = 0x20000. So we will want to copy the 0x20000 bytes following the header (that would be ROM file offsets 0x10-0x2000F). We just hightlight the first few, scroll down to the end, hold Shift and hightlight the last few bytes. Click Copy. Now go to the "new file" (the one with just the header), and go the first empty byte (0x10). Click Paste. Now scroll to the next unused byte (if we just added 0x20000 bytes, that would mean the cursor would need to be pasted at 0x20010). Again, click Paste. Now highlight, copy and paste the CHR, if it exists. (of course, if you were only expanding the CHR, you could just copy the header+PRG, paste in a new file, then paste the CHR twice). Update the header. (offset 0x00004 = number of PRG banks, 0x00005 = number of CHR banks) Now, save your new file, and test in an emulator. If the game still runs, chances are you have succeeded. Now, getting extra space: ****GETTING THE EXTRA SPACE***** Chances are good that only one of the two copies of the PRG is actually being used in the game. If you've found some data in the unexpanded ROM (such as text or something), try changing it at the same offset it was before. If it takes effect in game, you have succeeded. Otherwise, try the other copy ( (original offset)+(amount of extra ROM space added) ). It should take cause a change in the game. Now, you can probably clear out that half of the ROM (but back it up before you tamper with it, just to be safe). Note that regardless, it would be a good idea to leave the last 32KB (0x8000 bytes) of PRG alone. It might be only the last 16KB (0x4000 bytes), or 8KB (0x2000 bytes) that this warning applies to, but this last bank is usually "permanently" loaded into the emulated NES' RAM. Now, how do I use the extra ROM space? Well, that's a tough call, as it depends entirely on doing ASM hacks to the game (so it will involve breaking apart the target's game code to find out how and where the desired data, like maybe text is loaded). Then once you've found out where the data is loaded from, you'll need to write some ASM that'll move the appropiate ROM bank into memory, then process the data in the original game's format, and switch the original ROM bank back in when done. Let's do a sample code I make up to demonstrate text moving: (this is from no actual game, it's just some variables I made up) Say our game reads text from the ROM, then stores the text to a buffer that begins at $0500 in RAM, then writes that buffer to the screen. We only need to change the game to load our new text into the buffer. Once it's in the buffer, we can let the original coding do the work of drawing the text to the screen. LDA $8950,X ;the game is reading a pointer table. The lower ;byte of an entry. STA $45 LDA $8951,X ;the game is reading a pointer table. The higher ;byte of an entry. STA $46 ;now RAM $0045+$0046 contain the text string's ;pointer LDX #$00 LDY #$00 TEXT_LOOP: LDA ($45),Y ;the game is reading the string pointer CMP #$FF ;checking to see if we reached the "END" character BEQ END_TEXT ;skip if we have. STA $0500,X ;store the text to a buffer INX ;increase our position in the buffer INC $45 ;go to the next byte of the script, by increasing ;the low byte of the pointer LDA $45 ;check to see if the lower byte is now 0. BNE #$02 ;skip if low byte does not equal 0. INC $46 ;we're here if we got a 0 in the low byte, we must ;have hit a new hundreds value. We do this check so ;that 01FF + 1 = 0200, and not 0100. JMP TEXT_LOOP END_TEXT: JSR WRITE_TO_SCREEN What I'm going to need to do is seek out a document on the desired game's mapper. (NOTE THAT TO SUCCESSFULLY BANKSWAP, I WILL NEED TO FOLLOW THE DIRECTIONS SPECIFIC TO THE INDIVIDUAL GAME'S MAPPER) Say this game's mapper #19, Namcot 106 (which splits the PRG ROM into 8KB (0x2000) banks): To change the PRG: I must write the ROM bank number to To change RAM bank an address between: 8000-9FFF E000 and E7FF A000-BFFF E800 and EFFF C000-DFFF F000 and F7FF E000-FFFF I cannot change this bank. What I might do in the above example to JMP to some empty space in the current bank, or better yet, the unchangable bank if there's some space. Then I'll put in this code: LDA $8951,X ;checking the high byte of the pointer entry ;I might take advantage of the fact that valid ;NES pointers must be 8000 or higher, and assume ;that a lower value than 80 in the hundreds ;byte means to bankswitch in that value CMP #$80 BCS LOAD_TEXT ;skip bankswap routine if the hundreds byte is 80 ;or more, meaning we had a valid pointer, and no ;bankswap needed. LDA $50 ;let's say that RAM offset $0050 keeps track of what ;ROM bank is currently in 8000. PHA ;let's remember it (this line basically just stores ;the old contents of $50 to a temp variable. We could ;also STA to a normal RAM address). LDA $8951,X ;we decided that the high byte of our pointer is a ;bankswitch ROM number. STA $E000 ;so, let's swap it in. STA $50 ;and if it's now the contents of bank 8000, let's ;update our record of what ROM bank is in RAM bank ;8000 (this is something to consider in working with ;the "interrupt routine" that runs at the end of each ;frame. If we don't keep this address updated, it may ;start some loading data from the original ROM bank after ;the frame ends, leaving us with a mix of data from the ;new bank, followed by stuff from the old bank. LDA $8950,X ;we re-read the pointer table to get the proper pointer STA $45 ;which is found in the new bank. LDA $8951,X STA $46 LOAD_TEXT: LDX #$00 ;------------------------------------------------------ LDY #$00 ;this is the old loading code, which we repeat, since LDA ($45),Y ;it is how our theoretical game processess the text. CMP #$FF BEQ END_TEXT STA $0500,X INX INC $45 LDA $45 BEQ #$02 INC $46 JMP TEXT_LOOP ;----------------------------------------------------- PLA ;now to finish up, we grab our record of RAM 8000's ROM ;bank number off our "stack" of temp variables. STA $E000 ;we restore RAM 8000's original data STA $50 ;and update the record. JSR WRITE_TO_SCREEN ******WHAT IF THE CODE TO LOAD THE TEXT IS IN THE RAM BANK I NEED TO CHANGE?****** Ah, this is a tough one. The easiest way to do this would be to copy the data of the entire original ROM bank into the space of the new ROM bank. Then hopefully, there will be some data that is only accessed from the original ROM bank, and you can delete the new copy of the data, and paste text or whatever into some now unused space. The more difficult way is to switch the RAM bank anyway. The RAM is changed immediately after the bankswitch code (like STA $E000 for our example. This means that after STA $E000, the game will run whatever code is in the space corresponding. Say that the original bank was $8000-$9FFF of the ROM, and that STA $E000 (3 bytes) was run from $9176 in RAM. That means that the line 8D 00 E0 was found at $9176 in the ROM, which is $1176 bytes past the start of the bank ($8000). Say we are inserting ROM bank $20000-$21FFF. We will be able to make the game still work if we write the ASM at $21179 (the ROM bank starts at $20000, the last run opcode began at $1176 bytes past the start of the bank, and STA $E000 is 3 bytes long. Add those number together, and we get $21179. This can be very tricky to use, and it is only a good idea to re-write ASM code if it is an ASM routine small enough for us to know it will not need any code we cannot reproduce ourselves. Then once it's done, we need to find where our old code ended. Suppose our old routine ended at $91C4, which was $11C4 bytes past the start of its bank. How long is a bankswap? In our sample code, it is 6 bytes. We should write PLA:STA $50:STA $E000 (or 68 85 50 8D 00 E0) at ($11C4 -6 = $11BE) bytes past the start of our bank ($20000 + 11BE = 211BE). Note that since the bankswap code's effect is immediate, we want to write that code last. ****INTERRUPT: WHAT IF THE GAME LOADS THE ROM BANKS FROM A TABLE?**** This is an example of a problem I encountered while switching to expanded ROM banks in a hack I did of Megami Tensei, for Gideon Zhi. As you might know, (this is true of all games) at the end of each frame (not sure, but it's not too important to me when, just the fact that it happens), the game breaks off from the line of code it's executing to enter a predefined "interrupt" routine (this is probably used for timing reasons, to keep the game action running at a consistance pace). One hitch is that, near the end of the interrupt routine, MT reads a table at, say, $C940 (not actual just value, just for example) in RAM to find out which ROM bank to put in RAM bank $8000. And it reads another table at, say, $C950, to see what ROM bank it should put in RAM $A000. ---------------------------------------- SOME SETUP IN MY ACTUAL HACK CODE ITSELF Follow the example above about changing a RAM bank I'm working in. I'm deleting some ingame text at $18800 to make room for ASM. So I should use ROM $24800 (both instances, code begins 0800 bytes after the start of the bank). So, I NOP (hex $EA) the text at $18800. It bankswaps $24000 in, so now maybe I'm writing code at $2400A. It goes until maybe there's a code to swap back to the original bank at $2402A. So at $1802D (remember same addresses relative to the start of the bank), I have the game jump back to original routine. ---------------------------------------- So, the game code might look like this: LDA #$06 STA $8000 ;this is the Namcot 109 (mapper 76) code to prepare the ;mapper to change RAM $8000. LDA $C940,X STA $8001 ;this is what ROM bank to put in RAM $8000 LDA #$07 STA $8000 ;prepare the mapper to change RAM $A000 LDA $C950,X STA $8001 ;this is what ROM bank to put in RAM $A000 So, instead, I might put ROM bank $24000 (with a hack routine in place) in RAM $8000, and at $24000, I'd have a routine I wrote. So, I'd change the code to LDA #$06 STA $8000 ;changing RAM $8000.... LDA #$12 STA $8001 ;to hold ROM $24000 JMP $8000 ;moving to the routine Now, I'd need to find out what offset of code the game was at before it was interrupted. To figure that out, I'll need the stack. ------------------------------------------ STACK: This is like a collection of "temporary" variables. You should think of them stacked on top of each other, with the contents of RAM $01FF on the bottom, RAM $01FE on top of that, RAM $01FD on top of that, and so on... The stack pointer gives the last two digits of the RAM address of the value on top (on the NES, the first two digits are assumed to be 01). ------------------------------------------ Now, through some trial and error, I found that the address of the line of code that the game was on before the intterupt to be at the 5th and 6th byte below the top of the stack. So, I'll: PHX ;since X was used to figure the element in each table of ;what ROM bank to reload, I'll want to keep X. TSX ;find out what RAM address represents the top of the stack. LDA $0105,X ;(if 0100+X represents the top of the stack, then 0105+X will be 5 values down the stack). STA $CE ;keep a record of the return offset. LDA $0106,X STA $CF ;now RAM addresses 00CE+00CF is a record of the return ;address PLX ;now I can restore X to containing the element in the ;ROM bank # table. Now, since I'm NOT in the A000 bank, I can reload the expected value for RAM bank $A000. LDA #$07 STA $8000 LDA $C950,X STA $8001 JMP BACK_TO_INTERRUPT_CODE ;now I'm back in RAM $C000, so it's safe to change RAM bank $8000 LDA #$06 STA $8000 LDA $C940,X STA $8001 ...finish off interrupt. The game jumps back to the bank that was in RAM before any of my bank-switching shenanigans. But, it should be still in a field of NOPs. (remember, relative-offsetwise, my hack code in is the same position as a field of EA), until it hits code to go back to the game's original routine, like this EA EA EA EA EA EA EA 4C 86 9D -------------------- -------- NOP JMP back to game's original programming. So what I'm going to want to do is make a bankswap back to my hack bank before hitting the JMP so like: .. .. A9 12 8D 01 80 4C 86 9D -------------------- -------- This will cause the This is where the game to bank switch game returns to bank in to my hack when exiting my before it reaches hack's ROM bank. the JMP (4C) code. And this is what my hack bank looks like .. .. A9 0D 8D 01 80 EA EA EA -------------------- -------- This is a bank swap Empty space. code to go back to the original bank .. .. A9 12 8D 01 80 4C 86 9D ORIGINAL BANK -+ \ /| \/ /\ / \| -+ .. .. A9 0D 8D 01 80 EA EA EA HACK BANK So, now at my empty space: It's a simple matter of an indirect jump JMP ($CE), where RAM offset 00CE+00CF specified where the game should be if the interrupt hadn't interrupted